#include "GBDevice.h"
#include <assert.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "GBExternalFunctions.h"
#include "GBUtil.h"

#define GB_WIDTH 160
#define GB_HEIGHT 144
#define SGB_WIDTH 256
#define SGB_HEIGHT 224
#define MAX_FRAME_BUFFER 4
#define DEFAULT_MBC_ROM_BANK 1
#define DEFAULT_MBC_TIME_REGISTER 8
#define DEFAULT_MBC_LAST_ADDR 0x2000

typedef enum GBDeviceMode
{
	GBDeviceModeGameBoy,
	GBDeviceModeGameBoyColor,
	GBDeviceModeSuperSuperGameBoy
} GBDeviceMode;

struct GBDevice
{
	//GBDeviceMode mode; // assumed GameBoyColor
	size_t width;
	size_t height;
	GBCart * cart; // game cartridge

    uint8_t ram[0x10000 + 3]; // 64KB + 3B for invalid fetch operation
    uint8_t * rom;
    uint8_t * memoryRead[16]; // pointers for memory read
    uint8_t * memoryWrite[16]; // pointers for memory write
    uint8_t * vram; // vram for Gameboy Color
    uint8_t * wram; // work ram for gameboy color

	uint8_t * bmGrid; // image grid for rendering (160 * 144 + 8)
	uint8_t * bm2; // bm2 = bmGrid + 8 (for actual use)
	void * bmScr1;
	void * bmScr2;
	void * bmFinal;
	void * bmBorder;
	void * bmBorderFinal;
	void * bmBorderScreen;
	void * bmGBP;
	void * bmFilter;
	void * bmBuffer[MAX_FRAME_BUFFER];

    void (*initCPU)(GBDevice *, uint32_t);
    void (*initRAM)(GBDevice *, uint32_t);
    void (*initPalette)(GBDevice *);
    void (*drawScanLine)(GBDevice *);
    void (*updateScreen)(GBDevice *);
    void (*mbcHandler)(GBDevice *, uint16_t addr, uint8_t val);
};

static const uint8_t GB_NINTENDO_LOGO[] =
{
	0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B,
	0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D,
	0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E,
	0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99,
	0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC,
	0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E
};

GBDevice * GBDeviceCreate()
{
	GBDevice * self = NEW(GBDevice);
	CHECK(self);

	self->width = GB_WIDTH;
	self->height = GB_HEIGHT;

	// are the below needed? or are they specifically for os?
/*
	// add 8 bytes for cushion when x < 0 for the 1st line
	size_t bmGridSize = GB_WIDTH * GB_HEIGHT + 8;
	CHECK(self->bmGrid = (uint8_t *)malloc(bmGridSize));
	memset(self->bmGrid, 0, bmGridSize);
	self->bm2 = self->bmGrid + 8;

	CHECK(self->bmScr1 = GBBitmapCreate(GB_WIDTH, GB_HEIGHT));
	CHECK(self->bmScr2 = GBBitmapCreate(GB_WIDTH, GB_HEIGHT));
	self->bmFinal = self->bmScr1;

	CHECK(self->bmBorder = GBBitmapCreate(SGB_WIDTH, SGB_HEIGHT));
	CHECK(self->bmBorderFinal = GBBitmapCreate(SGB_WIDTH * 3, SGB_HEIGHT * 3));
	CHECK(self->bmBorderScreen = GBBitmapCreate(GB_WIDTH, GB_HEIGHT));
	CHECK(self->bmGBP = GBBitmapCreate(GB_WIDTH, GB_HEIGHT));
	CHECK(self->bmFilter = GBBitmapCreate(GB_WIDTH * 2, GB_HEIGHT * 2));

	for (int i = 0; i < MAX_FRAME_BUFFER; ++i)
		CHECK(self->bmBuffer[i] = GBBitmapCreate(GB_WIDTH, GB_HEIGHT));
*/
	return self;

ERROR:
	GBDeviceDestroy(self);
	fprintf(stderr, "GBDeviceCreate: error creating device\n");
	return 0;
}

void GBDeviceDestroy(GBDevice * self)
{
	if (!self)
		return;
	if (self->cart)
		GBDeviceUnloadROM(self);

	// following needed?
	if (self->bmGrid)
		DELETE(self->bmGrid);
	if (self->bmScr1)
		GBBitmapDestroy(self->bmScr1);
	if (self->bmScr2)
		GBBitmapDestroy(self->bmScr2);
	if (self->bmBorder)
		GBBitmapDestroy(self->bmBorder);
	// end

	DELETE(self);
}

uint16_t GBDeviceSetCartRAMSize(uint8_t code)
{
	if (code == 0x52)
	{
		return 0x480;
	}
	else if (code == 0x53)
	{
		return 0x500;
	}
	else if (code == 0x54)
	{
		return 0x600;
	}
	else if (code <= 0x8)
	{
		uint16_t result = 0x20;

		while (code--)
			result *= 0x2;

		return result;
	}
	else
	{
		return 0x20;
	}
}

uint16_t GBDeviceSetCartRAMSize(uint8_t code)
{
	if (code == 0x5)
	{
		return 0x20;
	}
	else if (code >= 0x1 && code <= 0x4)
	{
		uint16_t result = 0x2;
		code = code * 0x2 - 0x1;

		while (code--)
			result *= 0x2;

		return result;
	}
	else
	{
		return 0x0;
	}
}

bool GBDeviceIsCartHeaderValid(GBDevice * self)
{
	uint8_t cs = 0;

	for (size_t i = 0x134; i < 0x14D; ++i)
		cs = cs - buf[i] - 1;

	return cs == self->cart->headerChecksum;
}

bool GBDeviceIsCartValid(GBDevice * self)
{
	// TODO:
}

void GBDeviceLoadCartHeader(GBDevice * self)
{
	// title
	memcpy(self->ram + 0x134, self->cart->title, sizeof(self->cart->title) - 1);
	if (self->ram[0x14B] == 0x33)
		self->cart->title[11] = 0;

	// manufacturer code
	memcpy(self->ram + 0x13F, self->cart->manufacturer,
		sizeof(self->cart->manufacturer) - 1);

	// game type
	self->cart->isColor = self->ram[0x143] == 0x80 || self->ram[0x143] == 0xC0;
	self->cart->isSuper = self->ram[0x146] == 0x03 && self->ram[0x14B] == 0x33;

	// cartridge type
	self->cart->type = self->ram[0x147];

	// rom size
	self->cart->romSize = GBDeviceSetCartROMSize(self->ram[0x148]);

	// ram size (used)
	self->cart->ramSize = GBDeviceSetCartRAMSize(self->ram[0x149]);

	// country code
	self->cart->country = self->ram[0x14A];

	// licensee
	self->cart->licensee = self->ram[0x14B];
	if (self->cart->licensee == 0x33) // new licensee
		self->cart->licensee = self->ram[0x145] << 8 | self->ram[0x144];

	// mask rom version number
	self->cart->maskROMVersionNumber = self->ram[0x14C];

	// header checksum
	self->cart->headerChecksum = self->ram[0x14D];
	self->cart->isHeaderValid = GBDeviceIsCartHeaderValid(self);

	// global checksum
	self->cart->checksum = self->ram[0x14F] << 8 | self->ram[0x14E];
	self->cart->isValid = GBDeviceLoadCartHeaderIsValid(self);

	// pass Nintendo logo check
	self->cart->hasValidLogo = !memcmp(self->ram + 0x104, GB_NINTENDO_LOGO,
		sizeof(GB_NINTENDO_LOGO));

	// TODO: check commercial
}

enum GBCartID
{
    GBCartPokemonRedBlue2In1UnlSH1 = 0x8EEA1E50,
    GBCartPokemonRedBlue2In1UnlSH1A1 = 0x52F53853
};

void GBDeviceMemoryReadInit(GBDevice * self)
{
    memcpy(self->ram + 0x3000, self->rom + 0x3000, 0x1000);

    self->memoryRead[0] = self->rom;
    self->memoryRead[1] = self->rom + 0x1000;
    self->memoryRead[2] = self->rom + 0x2000;
    self->memoryRead[3] = self->rom + 0x3000;
    self->memoryRead[4] = self->rom + 0x4000;
    self->memoryRead[5] = self->rom + 0x5000;
    self->memoryRead[6] = self->rom + 0x6000;
    self->memoryRead[7] = self->rom + 0x7000;

    self->memoryRead[8] = self->vram;
    self->memoryRead[9] = self->vram + 0x1000;

    self->memoryRead[10] = self->cart->ram;
    self->memoryRead[11] = self->cart->ram + 0x1000;

    self->memoryRead[12] = self->wram;
    self->memoryRead[13] = self->wram + 0x1000;
    self->memoryRead[14] = self->wram;

    self->memoryRead[15] = self->ram + 0xF000;

    // To avoid reading an instruction *over* the ROM bank boundary
    // e.g., Hyper Lode Runner, Bo Jackson Hit and Run
    memcpy(self->ram + 0x4000, mrp[4], 3);
}

void GBDeviceMemoryWriteInit(GBDevice * self)
{
    self->memoryWrite[0] = self->rom;
    self->memoryWrite[1] = self->rom + 0x1000;
    self->memoryWrite[2] = self->rom + 0x2000;
    self->memoryWrite[3] = self->rom + 0x3000;
    self->memoryWrite[4] = self->rom + 0x4000;
    self->memoryWrite[5] = self->rom + 0x5000;
    self->memoryWrite[6] = self->rom + 0x6000;
    self->memoryWrite[7] = self->rom + 0x7000;

    self->memoryWrite[8] = self->vram;
    self->memoryWrite[9] = self->vram + 0x1000;

    self->memoryWrite[10] = self->cart->ram;
    self->memoryWrite[11] = self->cart->ram + 0x1000;

    self->memoryWrite[12] = self->wram;
    self->memoryWrite[13] = self->wram + 0x1000;
    self->memoryWrite[14] = self->wram;

    self->memoryWrite[15] = self->ram + 0xF000;
}

void GBMBCPokemon2In1Handler(GBDevice * self, uint16_t addr, uint8_t val)
{
    static int game = 0;
    static bool multi = false;

    if (addr< 0x2000) // RAM/RTC enable/disable
    {
        if (val == 0xC0 && !self->cart->mbc.gameSwitched)
        {
            multi = true;
        }
        else if (multi)
        {
            if (val == 0x0A)
            {
                if (game == 0)
                    return;

                self->rom = self->cart->rom + (game == 1 ? 0x8000 : 0x108000);
                GBDeviceMemoryReadInit(self);
                GBDeviceMemoryWriteInit(self);
                self->cart->gbc.gameSwitched = game;
                multi = false;
                game = 0;
            }
        }
    }
    else if (addr < 0x4000) // rom bank # (bit 0 to 6)
    {
        self->cart->
}

bool GBDeviceLoadSpecialCart(GBDevice * self)
{
    uint32_t id = (self->cart->checksum << 16) |
        (self->cart->headerChecksum << 8) | self->cart->title[0];

    switch (id)
    {
    case GBCartPokemonRedBlue2In1UnlSH1:
    case GBCartPokemonRedBlue2In1UnlSH1A1:
        if (self->ram[0x148] < 5) // SGB Pack (PD) (GB) (same id as [a1])
            break;
        self->cart->mbc.romSize = 2048;
        self->cart->mbc.type = GBCartMBCPokemon2In1;
        self->cart->mbc.hasRAM = true;
        self->cart->mbc.hasBattery = true;
        self->cart->mbc.hasTimer = true;
        self->cart->mbc.maxROMBank = self->cart->romSize / 16;
        self->cart->mbc.maxRAMBank = self->cart->ramSize / 8 - 1;
        self->mbcHandler = GBMBCPokemon2In1Handler;
        return true;

    case
}

bool GBDeviceLoadCartSetMBC(GBDevice * self)
{
    self->cart->mbc.romBank = DEFAULT_MBC_ROM_BANK;
    self->cart->mbc.timeRegister = DEFAULT_MBC_TIME_REGISTER;
    self->cart->mbc.lastAddr = DEFAULT_MBC_LAST_ADDR;

    if (GBDeviceLoadSpecialCart(self))
        return true;

    // TODO: ... try to load default cart
}

GBCart * GBDeviceLoadCartWithPath(GBDevice * self, char * path)
{
	if (!path)
		return 0;

	FILE * fp = fopen(path, "rb");
	CHECK(fp);

	CHECK(!fseek(fp, 0, SEEK_END));
	size_t len = ftell(fp);
	CHECK(!fseek(fp, 0, SEEK_SET));

	CHECK(self->cart = NEW(GBCart));
	CHECK(fread(self->ram, 0x150, 1, fp)); // load chunk into memory
	GBDeviceLoadCartHeader(self);
    CHECK(!GBDeviceLoadCartSetMBC(self));

	// TODO: rest...

	fclose(fp);

	return rom;

ERROR:
	if (self->cart)
		GBDeviceUnloadCart(self);

	if (fp)
		fclose(fp);

	fprintf(stderr, "GBDeviceLoadCartWithPath: error loading ROM\n");

	return 0;
}

void GBDeviceUnloadCart(GBDevice * self)
{
	if (self->cart)
	{
		DELETE(self->cart);
		self->cart = 0;
		memset(self->ram, 0, sizeof(self->ram));
	}
}

bool GBDeviceSaveStateWithPath(GBDevice * self, char * path)
{
	// TODO:
}

bool GBDeviceLoadStateWithPath(GBDevice * self, char * path)
{
	// TODO:
}
